---
id: auth-control
title: 15. 安全鉴权
sidebar_label: 15. 安全鉴权
---

## 15.1 什么是鉴权

**鉴权实际上就是一种身份认证**。

由用户提供凭据，然后将其与存储在操作系统、数据库、应用或资源中的凭据进行比较。 在授权过程中，如果凭据匹配，则用户身份验证成功，可执行已向其授权的操作。 授权指判断允许用户执行的操作的过程。
也可以将身份验证理解为进入空间（例如服务器、数据库、应用或资源）的一种方式，而授权是用户可以对该空间（服务器、数据库或应用）内的哪些对象执行哪些操作。

### 15.1.1 常见的鉴权方式

- `HTTP Basic Authentication`

这是 `HTTP` 协议实现的基本认证方式，我们在浏览网页时，从浏览器正上方弹出的对话框要求我们输入账号密码，正是使用了这种认证方式

- `Session + Cookie`

利用服务器端的 session（会话）和浏览器端的 cookie 来实现前后端的认证，由于 http 请求时是无状态的，服务器正常情况下是不知道当前请求之前有没有来过，这个时候我们如果要记录状态，就需要在服务器端创建一个会话(session),将同一个客户端的请求都维护在各自的会话中，每当请求到达服务器端的时候，先去查一下该客户端有没有在服务器端创建 session，如果有则已经认证成功了，否则就没有认证。

- `Token`

客户端在首次登陆以后，服务端再次接收 `HTTP` 请求的时候，就只认 `Token` 了，请求只要每次把 `Token` 带上就行了，服务器端会拦截所有的请求，然后校验 `Token` 的合法性，合法就放行，不合法就返回 401（鉴权失败）

`Token`验证比较灵活，适用于大部分场景。常用的 `Token` 鉴权方式的解决方案是 `JWT`，`JWT` 是通过对带有相关用户信息的进行加密，加密的方式比较灵活，可以根据需求具体设计。

- `OAuth`

OAuth（开放授权）是一个开放标准，允许用户授权第三方网站访问他们存储在另外的服务提供者上的信息，而不需要将用户名和密码提供给第三方网站或分享他们数据的所有内容，为了保护用户数据的安全和隐私，第三方网站访问用户数据前都需要显式的向用户征求授权。我们常见的提供 OAuth 认证服务的厂商有支付宝、QQ 和微信。

OAuth 协议又有 1.0 和 2.0 两个版本。相比较 1.0 版，2.0 版整个授权验证流程更简单更安全，也是目前最主要的用户身份验证和授权方式。

## 15.2 如何使用

:::info 配置之前

在添加授权服务之前，请先确保 `Startup.cs` 中 `Configure` 是否添加了以下两个中间件：

```cs
app.UseAuthentication();
app.UseAuthorization();
```

:::

### 15.2.1 添加 `Cookie` 身份验证

```cs
// Cookies单独身份验证
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
        .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, b =>
        {
            b.LoginPath = "/Home/Login";
        });
```

### 15.2.2 添加 `Jwt` 身份验证

```cs
services.AddJwt();
```

:::important 特别注意

`JWT` 鉴权并未包含在 `Furion` 框架中，需要安装 `Furion` 框架提供的 `Furion.Extras.Authentication.JwtBearer` 拓展包。

另外 `.AddJwt()` 必须在 `.AddControllers()` 之前注册。💡

:::

- 自定义 `Jwt` 配置（默认无需配置）

```json {4,6,8}
{
  "JWTSettings": {
    "ValidateIssuerSigningKey": true, // 是否验证密钥，bool 类型，默认true
    "IssuerSigningKey": "你的密钥", // 密钥，string 类型，必须是复杂密钥，长度大于16
    "ValidateIssuer": true, // 是否验证签发方，bool 类型，默认true
    "ValidIssuer": "签发方", // 签发方，string 类型
    "ValidateAudience": true, // 是否验证签收方，bool 类型，默认true
    "ValidAudience": "签收方", // 签收方，string 类型
    "ValidateLifetime": true, // 是否验证过期时间，bool 类型，默认true，建议true
    "ExpiredTime": 20, // 过期时间，long 类型，单位分钟，默认20分钟
    "ClockSkew": 5, // 过期时间容错值，long 类型，单位秒，默认 5秒
    "Algorithm": "HS256" // 加密算法，string 类型，默认 SecurityAlgorithms.HmacSha256
  }
}
```

:::warning 系统安全注意事项

`Furion` 框架为了方便开发，已经自动添加了 `Jwt` 默认配置。建议每个项目都应该单独配置 `IssuerSigningKey`，`ValidIssuer`，`ValidAudience` 这三个。否则同样用了 `Furion` 框架生成的 `Token` 可能存在相互访问各自系统的风险。

:::

:::important 温馨提示

目前支持的`加密算法`，详情请查阅[SecurityAlgorithms](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/blob/dev/src/Microsoft.IdentityModel.Tokens/SecurityAlgorithms.cs)

:::

- 生成 `Token`

```cs
// 生成 token
var accessToken = JWTEncryption.Encrypt(new Dictionary<string, object>()
            {
                { "UserId", user.Id },  // 存储Id
                { "Account",user.Account }, // 存储用户名
            });
```

### 15.2.3 混合身份验证

```cs
// JWT 和 Cookies 混合身份验证
services.AddJwt(options =>
{
      options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
      options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
{
       options.LoginPath = "/Home/Login";
});
```

:::important 特别注意

如果启用了混合身份验证后，`WebApi` 需在控制器/Action 中指定 `Scheme` 类型为 `JwtBearerDefaults.AuthenticationScheme`，如：

```cs {1}
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public class ApiServices : IDynamicApiController
{
}
```

如果不设置 `Scheme` 那么在混合授权的 `Swagger` 中将默认采用 `Cookie` 方式，也就是授权失败会将整个 `登录页面` 内容返回。

:::

## 15.3 高级自定义授权

`Furion` 框架提供了非常灵活的高级策略鉴权和授权方式，通过该策略授权方式可以实现任何自定义授权。

### 15.3.1 `AppAuthorizeHandler`

`Furion` 框架提供了 `AppAuthorizeHandler` 策略授权处理程序提供基类，只需要创建自己的 `Handler` 继承它即可。如：`JwtHandler`：

```cs {20,12}
using Furion.Authorization;
using Furion.Core;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.IdentityModel.JsonWebTokens;

namespace Furion.Web.Core
{
    /// <summary>
    /// JWT 授权自定义处理程序
    /// </summary>
    public class JwtHandler : AppAuthorizeHandler
    {
        /// <summary>
        /// 请求管道
        /// </summary>
        /// <param name="context"></param>
        /// <param name="httpContext"></param>
        /// <returns></returns>
        public override Task<bool> PipelineAsync(AuthorizationHandlerContext context, DefaultHttpContext httpContext)
        {
            // 此处已经自动验证 Jwt token的有效性了，无需手动验证

            // 检查权限，如果方法是异步的就不用 Task.FromResult 包裹，直接使用 async/await 即可
            return Task.FromResult(CheckAuthorzie(httpContext));
        }

        /// <summary>
        /// 检查权限
        /// </summary>
        /// <param name="httpContext"></param>
        /// <returns></returns>
        private static bool CheckAuthorzie(DefaultHttpContext httpContext)
        {
            // 获取权限特性
            var securityDefineAttribute = httpContext.GetMetadata<SecurityDefineAttribute>();
            if (securityDefineAttribute == null) return true;

            return "查询数据库返回是否有权限";
        }
    }
}
```

之后注册 `JwtHandler` 即可：

```cs
services.AddJwt<JwtHandler>();
```

### 15.3.2 完全自定义授权

有些时候可能针对不同的平台采用不一样的授权方式，比如合作信任的第三方机构可以免授权，这时候我们只需要重写 `HandleAsync` 方法即可。如：

```cs {11-25}
using Furion.Authorization;
using Furion.Core;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using System.Threading.Tasks;

namespace Furion.Web.Core
{
    public class JwtHandler : AppAuthorizeHandler
    {
        public override async Task HandleAsync(AuthorizationHandlerContext context)
        {
            // 常规授权（可以判断是不是第三方）
            var isAuthenticated = context.User.Identity.IsAuthenticated;

            // 第三方授权自定义
            if(是第三方){
                foreach (var requirement in pendingRequirements)
                {
                    // 授权成功
                   context.Succeed(requirement);
                }
            }
            // 授权失败
            else context.Fail();
        }
    }
}
```

## 15.4 授权特性及全局授权

默认情况下，所有的路由都是允许匿名访问的，所以如果需要对某个 `Action` 或 `Controller` 设定授权访问，只需要在 `Action` 或 `Controller` 贴 `[AppAuthorize]` 或 `[Authorize]` 特性即可。

如果需要对特定的 `Action` 或 `Controller` 允许匿名访问，则贴 `[AllowAnonymous]` 即可。

### 15.4.1 全局授权

```cs
services.AddJwt<JwtHandler>(enableGlobalAuthorize:true);
```

### 15.4.2 匿名访问

如果需要对特定的 `Action` 或 `Controller` 允许匿名访问，则贴 `[AllowAnonymous]` 即可。

## 15.5 自动刷新 Token

### 15.5.1 后端登录部分

当用户登录成功之后，返回 `accessToken` 字符串，之后通过 `JWTEncryption.GenerateRefreshToken()` 获取 `刷新Token`，并通过响应报文头返回，如：

```cs {9}
// token
var accessToken = JWTEncryption.Encrypt(new Dictionary<string, object>()
            {
                { "UserId", user.Id },  // 存储Id
                { "Account",user.Account }, // 存储用户名
            });

// 获取刷新 token
var refreshToken = JWTEncryption.GenerateRefreshToken(accessToken, 43200); // 第二个参数是刷新 token 的有效期（分钟），默认三十天

// 设置请求报文头
httpContextAccessor.HttpContext.Response.Headers["access-token"] = accessToken;
httpContextAccessor.HttpContext.Response.Headers["x-access-token"] = refreshToken;
```

用户登录成功之后把 `accessToken` 和 `refreshToken` 一起返回给客户端存储起来。

### 15.5.2 后端授权 `Handler` 部分

```cs {16-28}
using Furion.Authorization;
using Furion.Core;
using Furion.DataEncryption;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using System.Threading.Tasks;

namespace Furion.Web.Core
{
    /// <summary>
    /// JWT 授权自定义处理程序
    /// </summary>
    public class JwtHandler : AppAuthorizeHandler
    {
        /// <summary>
        /// 重写 Handler 添加自动刷新收取逻辑
        /// </summary>
        /// <param name="context"></param>
        /// <returns></returns>
        public override async Task HandleAsync(AuthorizationHandlerContext context)
        {
            // 自动刷新 token
            if (JWTEncryption.AutoRefreshToken(context, context.GetCurrentHttpContext()))
            {
                await AuthorizeHandleAsync(context);
            }
            else context.Fail();    // 授权失败
        }

        /// <summary>
        /// 验证管道，也就是验证核心代码
        /// </summary>
        /// <param name="context"></param>
        /// <param name="httpContext"></param>
        /// <returns></returns>
        public override Task<bool> PipelineAsync(AuthorizationHandlerContext context, DefaultHttpContext httpContext)
        {
            // 检查权限，如果方法时异步的就不用 Task.FromResult 包裹，直接使用 async/await 即可
            return Task.FromResult(true);
        }
    }
}
```

### 15.5.3 客户端部分

客户端每次请求需将 `accessToken` 和 `refreshToken` 放到请求报文头中传送到服务端，格式为：

```
Authorization: Bearer 你的token
X-Authorization: Bearer 你的刷新token
```

:::important 特别注意

在正常开发中，`refreshToken` 无需每次请求携带，而是 `accessToken` 即将过期之后携带即可。可以在客户端自行判断 `accessToken` 是否即将过期。

:::

如果 `Token` 过期，那么 `Furion` 将自动根据有效期内的 `refreshToken` 自动生成新的 `AccessToken`，并在 **响应报文头** 中返回，如：

```
access-token: 新的token
x-access-token: 新的刷新token
```

:::note 存储新的 Token

前端需要获取 **响应报文头** 新的 token 和刷新 token 替换之前在客户处存储旧的 token 和刷新 token。

:::

## 15.6 获取 `Jwt` 存储的信息

```cs
// 获取 `Jwt` 存储的信息
var userId = App.User?.FindFirstValue("键");
```

**注意引入 `System.Security.Claims` 命名空间**

## 15.7 反馈与建议

:::note 与我们交流

给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。

:::

---

:::note 了解更多

想了解更多 `鉴权授权` 知识可查阅 [ASP.NET Core - 安全和标识](https://docs.microsoft.com/zh-cn/aspnet/core/security/?view=aspnetcore-5.0) 章节。

:::
